package KNN;

import gui.GuiLogger;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import typydanych.Wektor;

public class KNN {

	private GuiLogger log;
	private IMetryka metryka;
	private List<Wektor> zbiorUczacy;
	private List<Wektor> zbiorTestowy;
	private int liczbaSasiadow;
	private boolean normalizuj;
	private double skutecznoscKlasyfikacji;
	private KNNListaNajblizszychSasiadow listaSasiadow;
	private HashMap<Byte, Integer> statystykaDobrze;
	private HashMap<Byte, Integer> statystykaZle;
	
	public static boolean PRZERWIJ = false;

	public KNN() {
		super();
		metryka = FabrykaMetryk.wezMetryke(FabrykaMetryk.EUKLIDESOWA);
		statystykaDobrze = new HashMap<Byte, Integer>();
		statystykaZle = new HashMap<Byte, Integer>();
		listaSasiadow = null;
		normalizuj = false;
	}
	
	public void setLogger(GuiLogger logger){
		log = logger;
	}
	
	
	
	public boolean isNormalizuj() {
		return normalizuj;
	}

	public void setNormalizuj(boolean normalizuj) {
		this.normalizuj = normalizuj;
	}

	public int getLiczbaSasiadow() {
		return liczbaSasiadow;
	}
	public void setLiczbaSasiadow(int liczbaSasiadow) {
		this.liczbaSasiadow = liczbaSasiadow;
	}
	public List<Wektor> getZbiorUczacy() {
		return zbiorUczacy;
	}
	public void setZbiorUczacy(List<Wektor> zbiorUczacy) {
		this.zbiorUczacy = zbiorUczacy;
	}
	public List<Wektor> getZbiorTestowy() {
		return zbiorTestowy;
	}
	public void setZbiorTestowy(List<Wektor> zbiorTestowy) {
		this.zbiorTestowy = zbiorTestowy;
	}
	public GuiLogger getLog() {
		return log;
	}
	public IMetryka getMetryka() {
		return metryka;
	}
	public void setMetryka(IMetryka metryka) {
		this.metryka = metryka;
	}
	public double getSkutecznoscKlasyfikacji(){
		return skutecznoscKlasyfikacji;
	}
	
	//trzeba zmienic typ wyjatku na Argument
	public void klasyfikuj() throws Exception{
		sprawdzPoprawnoscZbiorow();
		statystykaDobrze.clear();
		statystykaZle.clear();
		log.wypiszZKoncemWiersza("Rozpoczynam klasyfikacje z metryka: "+metryka);
		listaSasiadow = null; //nulluje, bo lista ma maksymalny rozmiar, ktory moglby byc inny niz w danym przebiegu
		int liczbaDobrzeZaklasyfikowanych = 0;
		int liczbaWektorowTestowych = zbiorTestowy.size();
		log.ustawMaksymalnaWartoscPostepu(liczbaWektorowTestowych);
		
		for (int i = 0; i < liczbaWektorowTestowych; i++) {

			Wektor wektorTestowy = zbiorTestowy.get(i);
			//normalizacja wektora testowego pochodzacego ze zbioru 10000
			if(normalizuj){
				wektorTestowy.normalizuj();
			}
			Wektor[] najblizsiSasiedzi = znajdzNajblizszychSasiadow(wektorTestowy, liczbaSasiadow);
			
			//przerywanie wykonania wymuszone przez GUI
			if(PRZERWIJ){
				return;
			}
			
			byte etykieta = przeprowadzGlosowanie(wektorTestowy, najblizsiSasiedzi);

			int licznikStatystyki = 1;
			if(poprawnaKlasyfikacjaWektora(wektorTestowy,etykieta)){
				liczbaDobrzeZaklasyfikowanych++;
				if (statystykaDobrze.get(etykieta) != null) {
					licznikStatystyki = statystykaDobrze.get(etykieta) + 1;
				}	
				statystykaDobrze.put(etykieta, licznikStatystyki);
			}else {
				log.wypiszZKoncemWiersza("Wzorzec "+i+" zle zaklasyfikowany. Jest \""+etykieta+"\" , a powinno byc \""+wektorTestowy.getEtykieta()+"\"");
				if (statystykaZle.get(etykieta) != null) {
					licznikStatystyki = statystykaZle.get(etykieta) + 1;
				}	
				statystykaZle.put(etykieta, licznikStatystyki);
			}
			skutecznoscKlasyfikacji = liczbaDobrzeZaklasyfikowanych/(double)(i+1) * 100.0;
			log.wypiszNaEtykiecie(String.valueOf(Math.round(skutecznoscKlasyfikacji))+"%");
			log.uaktualnijPostep(i+1);

		}
		
		log.wypiszZKoncemWiersza(wynikiStatystyki());
		
	}

	private String wynikiStatystyki() {
		StringBuffer str = new StringBuffer();
		str.append("Statystyka dobrze zaklasyfikowanych:\n");
		Iterator itDobrze = statystykaDobrze.entrySet().iterator();
		int wszystkichDobrzeSklasyfikowanych = 0;
		while (itDobrze.hasNext()) {
			Entry<Byte, Integer> dobrze = (Entry<Byte, Integer>) itDobrze.next();
			str.append(dobrze.getKey()+": "+dobrze.getValue()+"\n");
			wszystkichDobrzeSklasyfikowanych += dobrze.getValue();
		}
		str.append("Wszystkich dobrze sklasyfikowanych: "+wszystkichDobrzeSklasyfikowanych+"\n");
		str.append("\nStatystyka zle zaklasyfikowanych:\n");
		Iterator itZle = statystykaZle.entrySet().iterator();
		int wszystkichZleSklasyfikowanych = 0;
		while (itZle.hasNext()) {
			Entry<Byte, Integer> zle = (Entry<Byte, Integer>) itZle.next();
			str.append(zle.getKey()+": "+zle.getValue()+"\n");	
			wszystkichZleSklasyfikowanych += zle.getValue();
		}
		str.append("Wszystkich zle sklasyfikowanych: "+wszystkichZleSklasyfikowanych);
		return str.toString();
	}

	private void sprawdzPoprawnoscZbiorow() throws Exception {
		if(zbiorTestowy == null){
			throw new Exception("Niepoprawny zbior testowy - NULL");
		}else {
			if(zbiorTestowy.size()==0){
				throw new Exception("Niepoprawny zbior testowy - ilosc elementow=0");
			}
		}
		
		if(zbiorUczacy == null){
			throw new Exception("Niepoprawny zbior uczacy - NULL");
		}else {
			if(zbiorUczacy.size()==0){
				throw new Exception("Niepoprawny zbior uczacy - ilosc elementow=0");
			}
		}
		
		if(liczbaSasiadow <= 0){
			throw new Exception("Niepoprawny liczba sasiadow = "+liczbaSasiadow);
		}
	}

//	private Wektor[] znajdzNajblizszychSasiadow(Wektor wektor, int iloscNajblizszych){
//		Wektor[] najblizsiSasiedzi = new Wektor[iloscNajblizszych];
//		
//		//mapa przechowuje wartosci dystansow dla sasiadow (po indeksachw liscie)
//		Map<Integer, Double> mapaDystansow = new HashMap<Integer, Double>();
//		
//		//policz dystans dla kazdego sasiada
//		for(int s = 0; s < zbiorUczacy.size(); s++){
//			mapaDystansow.put(s, metryka.policzDystans(wektor, zbiorUczacy.get(s)));
//		}
//		
//		//szukamy k najblizszych sasiadow dla zadanego wektora
//		for ( int k = 0; k< iloscNajblizszych; k++){
//			double najmniejszyDystans = Double.MAX_VALUE;
//			int indeksNajmniejszego = -1;
//			Iterator it = mapaDystansow.entrySet().iterator();
//			while (it.hasNext()) {
//				Entry<Integer, Double> dystans = (Entry<Integer, Double>) it.next();
//				if (dystans.getValue() < najmniejszyDystans) {
//					najmniejszyDystans = dystans.getValue();
//					indeksNajmniejszego = dystans.getKey();
//				}
//			}
//			//znaleziono najmniejszego
//			najblizsiSasiedzi[k] = zbiorUczacy.get(indeksNajmniejszego);
//			mapaDystansow.remove(indeksNajmniejszego);
//		}
//		return najblizsiSasiedzi;
//	}
	
	private Wektor[] znajdzNajblizszychSasiadow(Wektor wektor, int iloscNajblizszych){
		
		if(listaSasiadow == null){
			listaSasiadow = new KNNListaNajblizszychSasiadow(iloscNajblizszych);
		}
		
		for(int s = 0; s < zbiorUczacy.size(); s++){
			if(PRZERWIJ){
				return null;
			}
			Wektor sasiad = zbiorUczacy.get(s);
			if(normalizuj && !sasiad.isZnormalizowany()){
				sasiad.normalizuj();
			}
			MetrykaWektora nowyElement = new MetrykaWektora(sasiad, metryka.policzDystans(wektor, sasiad));
			listaSasiadow.dodaj(nowyElement);
		}
		Wektor[] najblizsiSasiedzi = listaSasiadow.kolekcja();
		listaSasiadow.wyczysc();
		return najblizsiSasiedzi;
	}
	
	
	private byte przeprowadzGlosowanie(Wektor wektor, Wektor[] sasiedzi){
		
		//mapa glosow. indeksem jest etykieta i ma pewna ilosc glosow
		Map<Byte, Integer> mapaGlosow = new HashMap<Byte, Integer>();
		//zmienne wykorzystywane do ustalania zwyciezcy
		byte etykietaWygyrwajaca = -1;
		int maksymalnaLiczbaglosow = 0;
		
		for (int i = 0; i < sasiedzi.length; i++) {
			
			byte aktualnaEtykieta = sasiedzi[i].getEtykieta();
			int aktualnaliczbaGlosow = 1;
			
			if (mapaGlosow.get(aktualnaEtykieta) != null) 
				aktualnaliczbaGlosow = mapaGlosow.get(aktualnaEtykieta) + 1;
			
			mapaGlosow.put(aktualnaEtykieta, aktualnaliczbaGlosow);
			
			//uaktualniamy zwyciezce
			if(maksymalnaLiczbaglosow < aktualnaliczbaGlosow){
				maksymalnaLiczbaglosow = aktualnaliczbaGlosow;
				etykietaWygyrwajaca = (byte) aktualnaEtykieta;
			}	
		}
		
		//co w przypadku jesli jest kilku zwycezcow glosowania
		Map<Byte, Double> mapaSrednich = sprawdzCzyIstniejeInnyZwyciezca(wektor, sasiedzi, mapaGlosow, etykietaWygyrwajaca, maksymalnaLiczbaglosow);
		if(mapaSrednich != null){
			etykietaWygyrwajaca = wytypujLepszegoZwyciezce(mapaSrednich);
		}
		
		
		return etykietaWygyrwajaca;
	}
	
	private byte wytypujLepszegoZwyciezce(Map<Byte, Double> mapaSrednich) {

		double minSrednia = Double.MAX_VALUE;
		byte lepszaEtykieta = -1;
		Iterator it = mapaSrednich.entrySet().iterator();
		while (it.hasNext()) {
			Entry<Byte, Double> sredniaDystansu = (Entry<Byte, Double>) it.next();
			if (sredniaDystansu.getValue() < minSrednia) {
				lepszaEtykieta = sredniaDystansu.getKey();
				minSrednia = sredniaDystansu.getValue();
			}
		}
		return lepszaEtykieta;
	}

	private Map<Byte, Double> sprawdzCzyIstniejeInnyZwyciezca(Wektor wektor, Wektor[] sasiedzi, Map<Byte, Integer> mapaGlosow, byte etykietaKandydata, int liczbaGlosowKandydata) {
		
		Iterator it = mapaGlosow.entrySet().iterator();
		Map<Byte, Double> mapaSrednichKandydatow = null;
		boolean wieleKandydatow = false;
		while (it.hasNext()) {
			Entry<Byte, Integer> glos = (Entry<Byte, Integer>) it.next();
			if(glos.getValue() == liczbaGlosowKandydata){
				if(glos.getKey()!= etykietaKandydata){
					wieleKandydatow = true;
					if(mapaSrednichKandydatow == null){
						mapaSrednichKandydatow = new HashMap<Byte, Double>();
					}
					mapaSrednichKandydatow.put(glos.getKey(), policzSredniaDlaKandydata(wektor, sasiedzi, glos.getKey()));
				}
			}
		}
		if(wieleKandydatow){
			mapaSrednichKandydatow.put(etykietaKandydata, policzSredniaDlaKandydata(wektor, sasiedzi, etykietaKandydata));
		}
		
		
		return mapaSrednichKandydatow;
	}

	private Double policzSredniaDlaKandydata(Wektor wektor, Wektor[] sasiedzi, byte etykieta) {
		double suma = 0.0;
		double iloscKandydatow = 0.0;
		for (int i = 0; i < sasiedzi.length; i++) {
			Wektor sasiad = sasiedzi[i];
			if(sasiad.getEtykieta() == etykieta){
				iloscKandydatow += 1.0;
				suma += metryka.policzDystans(wektor, sasiad);
			}
		}
		return suma/iloscKandydatow;
	}

	private boolean poprawnaKlasyfikacjaWektora(Wektor wektor, byte etykieta){
		if(etykieta == wektor.getEtykieta()){
			return true;
		}
		return false;
	}
	
	
	
	
}
